#include "mitab_freelist.h"
#include "mitab_priv.h"
#include "cpl_error.h"
#include "cpl_vsi.h"
#include "string"


TABIDTFile::TABIDTFile() : m_fpIDT(NULL)
{
}

TABIDTFile::~TABIDTFile()
{
    Close();
}

bool TABIDTFile::Open(const char * pszFname)
{
    // Sanity check
    if(pszFname == NULL)
        return false;

    char * pszTempFname = (char *)VSIMalloc(strlen(pszFname) + 1);
    strcpy(pszTempFname, pszFname);
    strcpy(pszTempFname + strlen(pszFname) - 3, "idf"); 
    if(!m_fileIndex.Open(pszTempFname))
    {
        CPLError(CE_Failure, CPLE_FileIO,
            "Open() failed for %s", pszTempFname);
        VSIFree(pszTempFname);
        return false;
    }
    VSIFree(pszTempFname);

    bool bCreateFile = !VSIFileExists(pszFname);
    if(!bCreateFile)
        m_fpIDT = CPLOpenShared(pszFname, "rb+", 0);
    else
         m_fpIDT = CPLOpenShared(pszFname, "wb+", 0);
   /* if(m_fpIDT == NULL)
    {
        m_fpIDT = VSIFOpen(filename, "wb+");
        bCreateFile = true;
    }*/
    if(m_fpIDT == NULL)
    {
        CPLError(CE_Failure, CPLE_FileIO,
            "Open() failed for %s", pszFname);
        return false;
    }

    // Check the header of the file
    if(!bCreateFile)
    {
        VSIFRead(&m_header, sizeof(TABIDTFileHeader), 1, m_fpIDT);
        if(m_header.m_magic != IDT_FILE_MAGIC)
        {
            CPLError(CE_Failure, CPLE_FileIO,
                "File Format does not match");
            return false;
        }
    }
    else
    {
        m_header.m_magic = IDT_FILE_MAGIC;
        m_header.m_tTimeStamp = 0;
        VSIFWrite(&m_header, sizeof(TABIDTFileHeader), 1, m_fpIDT);
        VSIFFlush(m_fpIDT);
    }

    // TODO: Fix file name extension
    m_pszFname = CPLStrdup(pszFname);

    // TODO: check whether index file has content. 
    // If not, we are suppose to rebuild the index

    return true;
}


void TABIDTFile::Close(GInt32 timeStamp)
{
    if(m_fpIDT == NULL)
        return;

    Flush(timeStamp);

    m_fileIndex.Close();

    CPLCloseShared(m_fpIDT);
    m_fpIDT = NULL;
}

int TABIDTFile::Flush(GInt32 timeStamp)
{
    if(m_fpIDT == NULL)
        return 0;

    // Write time stamp;
    if(timeStamp)
        m_header.m_tTimeStamp = timeStamp;

    VSIFSeek(m_fpIDT, 0, SEEK_SET);
    VSIFWrite(&m_header, sizeof(TABIDTFileHeader), 1, m_fpIDT);

    m_fileIndex.Flush();

    return VSIFFlush(m_fpIDT);
}

int TABIDTFile::Destory()
{
    char* pszFname = CPLStrdup(m_pszFname);
    int ret1 = m_fileIndex.Destory();
    Close();
    int ret2 = VSIUnlink(pszFname);
    CPLFree(pszFname);
    return (ret1 == 0 && ret2 == 0 ? 0 : 1);
}

int TABIDTFile::IsFileReadOnly()
{
    if (NULL == m_pszFname || m_pszFname[0] == 0)
        return -1;

    return VSIIsFileReadOnly(m_pszFname);
}

GInt32 TABIDTFile::Allocate(GInt32 size)
{
    // Sanity check
    if(size <= 0)
        return 0;

    // We 'll search from the back, it'll require less moving of the content of the file if the
    // deleted entries are at the end of the file.
    VSIFSeek(m_fpIDT, 0, SEEK_END);
    while(VSIFSeek(m_fpIDT, -(int)sizeof(TABIDTFileEntry), SEEK_CUR) == 0 && VSIFTell(m_fpIDT) > 0)
    {
        TABIDTFileEntry entry;
        if(VSIFRead(&entry, sizeof(TABIDTFileEntry), 1, m_fpIDT) != sizeof(TABIDTFileEntry))
            return 0;
        
        // We are suppose to find an avalable space, ignore the deleted ones
        if(entry.m_bIsDeleted)
            continue;

        // The size of the free space must be lager than what we require
        if(entry.m_szFreeSpace >= size)
        {
            // Bingo! This is it, we'll delete this entry from the file if the size is just ok
            GInt32 ret = entry.m_offsetInMapFile;

            if(entry.m_szFreeSpace > size)
            {
                entry.m_offsetInMapFile += size;
                entry.m_szFreeSpace -= size;
            }
            else
            {
                // Set the delete mark for the entry
                entry.m_bIsDeleted = true;
                VSIFSeek(m_fpIDT, -(int)sizeof(TABIDTFileEntry), SEEK_CUR);
                if(VSIFWrite(&entry, sizeof(TABIDTFileEntry), 1, m_fpIDT) != sizeof(TABIDTFileEntry))
                {
                    // Failed to write the entry
                    // Shall we just return?
                    return true;
                }

                // Addjust the linked list
                if(entry.m_nextFreeSpaceInBlock != 0)
                {
                    // Next
                    TABIDTFileEntry next;
                    GetEntry(entry.m_nextFreeSpaceInBlock, &next);
                    next.m_prevFreeSpaceInBlock = entry.m_prevFreeSpaceInBlock;
                    SetEntry(entry.m_prevFreeSpaceInBlock, &next);
                }

                if(entry.m_prevFreeSpaceInBlock != 0)
                {
                    // Prev
                    TABIDTFileEntry prev;
                    GetEntry(entry.m_prevFreeSpaceInBlock, &prev);
                    prev.m_nextFreeSpaceInBlock = entry.m_nextFreeSpaceInBlock;
                    SetEntry(entry.m_prevFreeSpaceInBlock, &prev);
                }
                else
                {
                    // Well, this is the first one of the linked list which the index points to
                    // We are suppose to update the index after we deleted this entry
                    m_fileIndex.SetBlockPtr(entry.m_blockOffset, entry.m_nextFreeSpaceInBlock);
                }
            }

            return ret;
        }
    }

    return 0;

}

GInt32 TABIDTFile::GetNextBlock(GInt32 nBlockAddress, FILE * fpMap)
{
    TABMAPCoordBlock block;
    block.ReadFromFile(fpMap , nBlockAddress);
    return block.GetNextCoordBlock();
}

bool TABIDTFile::Free(FILE * fpMap, GInt32 nBlockAddress, GInt32 offsetInMapFile, GInt32 size, TABBinBlockManager * pBlockManager)
{
    // sanity check
    if (size <= 0)
        return false;

    if(size == 512)
    {
        // TODO: add the block into free list
        pBlockManager->PushGarbageBlock(nBlockAddress);
        return true;
    }

    if(size > 512)
    {
        // There are more than a block need to be freed
        //

        int sz = 512 - offsetInMapFile % 512;
        if(! Free(fpMap, nBlockAddress, offsetInMapFile, sz, pBlockManager))
            return false;
        size -= sz;
        nBlockAddress = offsetInMapFile = GetNextBlock(nBlockAddress, fpMap);

        while(size)
        {
            if(size >= 512)
            {
                if(! Free(fpMap, nBlockAddress, offsetInMapFile, 512, pBlockManager))
                    return false;
                size -= 512;
                nBlockAddress = offsetInMapFile = GetNextBlock(nBlockAddress, fpMap);                
            }
            else
            {
                return Free(fpMap, nBlockAddress, offsetInMapFile, size, pBlockManager);
            }
        }

        return true;
    }

    GInt32 idPtr = m_fileIndex.GetBlockPtr(nBlockAddress);
    TABIDTFileEntry firstEntry;
    GetEntry(idPtr, &firstEntry);
    bool bInsert = !idPtr;
    
    GInt32 ptr = idPtr;
    TABIDTFileEntry entry;
    bool bIsMerged = false;
    GInt32 ptrMerged;
    TABIDTFileEntry entryMerged;
    
    // Check whether we can merge the free space
    while(ptr)
    {
        GetEntry(ptr, &entry);

        // For the following 2 cases, the 2 entries are supposed to be merged into one
        // Notice: once we find an entry to merge, we shall still continue. We still need 
        // to scan the whole linked list for there might be 2 entries to merge
        if(entry.m_offsetInMapFile + entry.m_szFreeSpace == offsetInMapFile)
        {
            if(!bIsMerged)
            {
                entry.m_szFreeSpace += size;
                SetEntry(ptr, &entry);
                bIsMerged = true;
                entryMerged = entry;
                ptrMerged = ptr;
            }
            else
            {
                entryMerged.m_szFreeSpace += entry.m_szFreeSpace;
                entryMerged.m_offsetInMapFile = entry.m_offsetInMapFile;
                SetEntry(ptrMerged, &entryMerged);

                // Remove entry for it is merged
                DeleteEntry(ptr);
                break;
            }
        }
        else if(offsetInMapFile + size == entry.m_offsetInMapFile)
        {
            if(!bIsMerged)
            {
                entry.m_szFreeSpace += size;
                entry.m_offsetInMapFile = offsetInMapFile;
                SetEntry(ptr, &entry);
                bIsMerged = true;
                entryMerged = entry;
                ptrMerged = ptr;
            }
            else
            {
                entryMerged.m_szFreeSpace += entry.m_szFreeSpace;
                SetEntry(ptrMerged, &entryMerged);

                // Remove entry for it is merged
                DeleteEntry(ptr);
                break;
            }
        }

        ptr = entry.m_nextFreeSpaceInBlock;
    }

    if(!bIsMerged)
    {
        // we didn't find an entry to merge, so we need allocate a new slot for it
        // Also, now the file pointer should points to the end of entry, 
        GInt32 entryPtr = VSIFTell(m_fpIDT) - sizeof(TABIDTFileEntry);
        TABIDTFileEntry newEntry;
        newEntry.m_bIsDeleted = false;
        newEntry.m_blockOffset = nBlockAddress;
        newEntry.m_offsetInMapFile = offsetInMapFile;
        newEntry.m_szFreeSpace = size;
        newEntry.m_nextFreeSpaceInBlock = 0;
        newEntry.m_prevFreeSpaceInBlock = bInsert ? 0 : entryPtr;
        
        // Move to the end of file and insert an entry
        VSIFSeek(m_fpIDT, 0, SEEK_END);
        VSIFWrite(&newEntry, sizeof(TABIDTFileEntry), 1, m_fpIDT);

        if(!bInsert)
        {
            entry.m_nextFreeSpaceInBlock = VSIFTell(m_fpIDT) - sizeof(TABIDTFileEntry);
            SetEntry(entryPtr, &entry);        
        }
        else
        {
            // this is the first entry of the block, we need update the index
            m_fileIndex.SetBlockPtr(nBlockAddress, VSIFTell(m_fpIDT) - sizeof(TABIDTFileEntry));
        }
    }
    return true;
}

void TABIDTFile::DeleteEntry(GInt32 nEntryPtr)
{
    GBool bDeleted = TRUE;
    VSIFSeek(m_fpIDT, nEntryPtr, SEEK_SET);
    VSIFWrite(&bDeleted, sizeof(GBool), 1, m_fpIDT);
}

bool TABIDTFile::GetEntry(GInt32 offset, TABIDTFileEntry * pEntry)
{
    VSIFSeek(m_fpIDT, offset, SEEK_SET);
    return VSIFRead(pEntry, sizeof(TABIDTFileEntry), 1, m_fpIDT) == sizeof(TABIDTFileEntry);
}

bool TABIDTFile::SetEntry(GInt32 offset, TABIDTFileEntry * pEntry)
{
    VSIFSeek(m_fpIDT, offset, SEEK_SET);
    return VSIFWrite(pEntry, sizeof(TABIDTFileEntry), 1, m_fpIDT) == sizeof(TABIDTFileEntry);
}


TABIDTFileIndex::TABIDTFileIndex(): m_fpIndex(NULL)
{
}

TABIDTFileIndex::~TABIDTFileIndex()
{
    Close();
}

bool TABIDTFileIndex::Open(const char * pszFname)
{
    m_fpIndex = VSIFOpen(pszFname, "rb+");
    if(m_fpIndex == NULL)
        m_fpIndex = VSIFOpen(pszFname, "wb+");
    if(m_fpIndex == NULL)
    {
        CPLError(CE_Failure, CPLE_FileIO,
            "Open() failed for %s", pszFname);
        return false;
    }

    m_pszFname = CPLStrdup(pszFname);
    return true;
}

void TABIDTFileIndex::Close()
{
    if(m_fpIndex == NULL)
        return;

    Flush();

    VSIFClose(m_fpIndex);
    m_fpIndex = NULL;

    CPLFree(m_pszFname);
    m_pszFname = NULL;
    // TODO: try to remove deleted records
}

int TABIDTFileIndex::Flush()
{
    if(m_fpIndex == NULL)
        return 0;

    return VSIFFlush(m_fpIndex);
}

int TABIDTFileIndex::Destory()
{
    char* pszFname = CPLStrdup(m_pszFname);
    Close();
    int ret = VSIUnlink(pszFname);
    CPLFree(pszFname);
    return ret;
}

int TABIDTFileIndex::IsFileReadOnly()
{
    if (NULL == m_pszFname || m_pszFname[0] == 0)
        return -1;

    return VSIIsFileReadOnly(m_pszFname);
}

GInt32 TABIDTFileIndex::GetBlockPtr(GInt32 nBlockAddress)
{
    TABIDTFileIndexEntry entry;

    VSIFSeek(m_fpIndex, 0, SEEK_SET);
    while(VSIFRead(&entry, sizeof(TABIDTFileIndexEntry), 1, m_fpIndex))
    {
        if(nBlockAddress == entry.m_nBlockAddress)
            return entry.m_nIndexPtr;
    }
    return 0;
}

void TABIDTFileIndex::SetBlockPtr(GInt32 nBlockAddress, GInt32 nBlockPtr)
{
    TABIDTFileIndexEntry entry;

    VSIFSeek(m_fpIndex, 0, SEEK_SET);
    while(VSIFRead(&entry, sizeof(TABIDTFileIndexEntry), 1, m_fpIndex))
    {
        if(nBlockAddress == entry.m_nBlockAddress)
        {
            // Ok, we found it
            entry.m_nIndexPtr = nBlockPtr;
            VSIFSeek(m_fpIndex, -(int)sizeof(TABIDTFileIndexEntry), SEEK_SET);
            VSIFWrite(&entry, sizeof(TABIDTFileIndexEntry), 1, m_fpIndex);
            return;
        }
    }

    // Not found? we append it at the end of the file
    entry.m_nBlockAddress = nBlockAddress;
    entry.m_nIndexPtr = nBlockPtr;
    VSIFWrite(&entry, sizeof(TABIDTFileIndexEntry), 1, m_fpIndex);
    return;
}
